﻿using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace SqueezeboxLib
{
    /// <summary>
    /// Represents a <b>SqueezeboxServer</b>
    /// </summary>
    public class SqueezeboxServer
    {
        private Thread messageThread;
        private NetworkStream networkStream;
        private readonly TcpClient tcpClient;
        private readonly byte[] readBuffer = new byte[1024];

        /// <summary>
        /// Occurs when the server receives a message
        /// </summary>
        public event EventHandler<MessageReceivedEventArgs> MessageReceived;

        // TODO: Hook up notifications from the message thread
        #region Notifications

        /// <summary>
        /// Occurs when a new client joins or when a client disconnects or connects.
        /// </summary>
        public event EventHandler<ClientStatusChangedEventArgs> ClientStatusChanged;

        /// <summary>
        /// Occurs when a 'rescan' or 'wipecache' has happened.
        /// </summary>
        public event EventHandler RescanComplete;

        /// <summary>
        /// Occurs when a library has changed.
        /// </summary>
        public event EventHandler LibraryChanged;

        /// <summary>
        /// Occurs when an IR code unknown by the server is signaled.
        /// </summary>
        public event EventHandler<UnknownIREventArgs> UnknownIR;

        /// <summary>
        /// Occurs at the start of a new song.
        /// </summary>
        public event EventHandler<PlaylistNewSongEventArgs> PlaylistNewSong;

        /// <summary>
        /// Occurs when the playlist is stopped.
        /// </summary>
        public event EventHandler PlaylistStop;

        /// <summary>
        /// Occurs when the playlist is paused.
        /// </summary>
        public event EventHandler PlaylistPause;

        /// <summary>
        /// Occurs when a preference is changed.
        /// </summary>
        public event EventHandler<PreferenceChangedEventArgs> PreferenceChanged;

        /// <summary>
        /// Occurs when the favorite database is changed.
        /// </summary>
        public event EventHandler FavoritesChanged;

        /// <summary>
        /// Occurs when the alarm state changes
        /// </summary>
        public event EventHandler<AlarmStateChangedEventArgs> AlarmStateChanged;

        public event EventHandler PlayersUpdated;

        public event EventHandler ConnectionEstablished;

        #endregion

        /// <summary>
        /// Gets the Squeezebox Server IP
        /// </summary>
        public string IP { get; private set; }

        /// <summary>
        /// Gets the Squeezebox Server port
        /// </summary>
        public int Port { get; private set; }

        /// <summary>
        /// Gets the players on the server, where the key is the playerID and the value is the player.
        /// </summary>
        public Dictionary<string, SqueezeboxPlayer> Players { get; private set; }

        /// <summary>
        /// Gets the Squeezebox Server Database
        /// </summary>
        public SqueezeboxDatabase Database { get; private set; }

        public SqueezeboxServer(string ip, int port)
        {
            IP = ip;
            Port = port;

            Players = new Dictionary<string, SqueezeboxPlayer>();

            Database = new SqueezeboxDatabase(Send, MessageReceived);

            tcpClient = new TcpClient();
        }

        public void Connect()
        {
            tcpClient.Connect(IP, Port);

            networkStream = tcpClient.GetStream();

            messageThread = new Thread(MessageThread);
            messageThread.Start();

            if (ConnectionEstablished != null)
            {
                ConnectionEstablished(this, new EventArgs());
            }
        }

        public void Disconnect()
        {
            tcpClient.Close();
        }

        public void Send(SqueezeboxCommand command)
        {
            Send(command.Command);
        }

        public void Send(string message)
        {
            Byte[] messageBuffer = Encoding.UTF8.GetBytes(message + "\n");

            Send(messageBuffer);
        }

        private void Send(Byte[] command)
        {
            if (networkStream.CanWrite)
            {
                networkStream.BeginWrite(command, 0, command.Length, WriteCallback, networkStream);
            }
            else
            {
                throw new Exception("Unable to send message to the server at this moment");
            }
        }

        private void WriteCallback(IAsyncResult iar)
        {
            NetworkStream stream = (NetworkStream)iar.AsyncState;
            stream.EndWrite(iar);
        }

        private void MessageThread()
        {
            bool keepAlive = true;
            while (keepAlive)
            {
                if (networkStream != null && networkStream.CanRead)
                {
                    StringBuilder codedMessages = new StringBuilder();

                    do
                    {
                        int bytesRead;
                        try
                        {
                            bytesRead = networkStream.Read(readBuffer, 0, readBuffer.Length);
                        }
                        catch (Exception)
                        {
                            keepAlive = false;
                            break;
                        }


                        if (bytesRead == 0)
                        {
                            keepAlive = false;
                            break;
                        }

                        codedMessages.AppendFormat("{0}", Encoding.UTF8.GetString(readBuffer, 0, bytesRead));
                    }
                    while (networkStream.DataAvailable);

                    string decodedMessages = System.Web.HttpUtility.UrlDecode(codedMessages.ToString());
                    foreach (string message in decodedMessages.Split('\n'))
                    {
                        if (message.Length == 0)
                        {
                            continue;
                        }

                        string[] responseSplit = message.Split(' ');

                        if (responseSplit.Length > 0)
                        {
                            switch (responseSplit[0])
                            {
                                case "players":
                                    for (int i = 4; i < responseSplit.Length; i++)
                                    {
                                        TaggedParameter taggedParameter = TaggedParameter.Parse(responseSplit[i]);

                                        if (taggedParameter.Tag == "playerindex") // delimited
                                        {
                                            TaggedParameter newPlayerID = TaggedParameter.Parse(responseSplit[++i]);

                                            AddPlayer(newPlayerID.Parameter);
                                        }
                                    }

                                    if (PlayersUpdated != null)
                                    {
                                        PlayersUpdated(this, new EventArgs());
                                    }

                                    break;
                            }

                            // Client Status Change
                            if (responseSplit.Length > 1 && responseSplit[1] == "client")
                            {
                                ClientStatus clientStatus;
                                Enum.TryParse(responseSplit[2].Replace(Environment.NewLine, string.Empty), true,
                                              out clientStatus);

                                string playerID = System.Web.HttpUtility.UrlDecode(responseSplit[0]);

                                switch (clientStatus)
                                {
                                    case ClientStatus.New:
                                    case ClientStatus.Reconnect:
                                        AddPlayer(playerID);
                                        break;
                                    case ClientStatus.Disconnect:
                                        RemovePlayer(playerID);
                                        break;
                                    default:
                                        throw new ArgumentOutOfRangeException();
                                }

                                if (ClientStatusChanged != null)
                                {
                                    ClientStatusChanged(this, new ClientStatusChangedEventArgs(playerID, clientStatus));
                                }
                            }

                            // Check if anyone has subscribed to the event, then call it if anyone has subscribed to the event.
                            if (MessageReceived != null)
                            {
                                MessageReceived(this, new MessageReceivedEventArgs(message));
                            }
                        }

                    }

                    Thread.Sleep(100);
                }
            }
        }

        public void RemovePlayer(string playerID)
        {
            if (Players.ContainsKey(playerID))
            {
                Players.Remove(playerID);
            }
        }

        public void AddPlayer(string playerID)
        {
            if (!Players.ContainsKey(playerID))
            {
                Players.Add(playerID, new SqueezeboxPlayer(playerID, this));
            }
        }

        #region General commands and queries

        /// <summary>
        /// Authenticate yourself on the server. 
        /// If unsuccessful, the server disconnects. 
        /// If security is off it will allways succeed.
        /// </summary>
        /// <param name="username"></param>
        /// <param name="password"></param>
        public void Login(string username, string password)
        {
            Send("login " + username + " " + password);
        }

        /// <summary>
        /// Determine if the command or query indicated by "requestTerms" is available.
        /// </summary>
        /// <param name="requestTerms"></param>
        public void Can(string requestTerms)
        {
            Send("can " + requestTerms + " ?");
        }

        /// <summary>
        /// Query the version number of Squeezebox Server.
        /// </summary>
        public void Version()
        {
            Send("version ?");
        }

        /// <summary>
        /// Enables to receive asynchronously internal server notifications.
        /// </summary>
        /// <param name="mode">
        /// Use 0 to clear, 1 to set, ? to query, 
        /// and no parameter to toggle the listen state of the connection.
        /// </param>
        public void Listen(string mode)
        {
            Send("listen " + mode);
        }

        /// <summary>
        /// Similar to Listen. Enables to receive asynchronously to only the given internal server notifications.
        /// </summary>
        /// <param name="notificationsOfIntrest">
        /// Notifications to subscribe to. 
        /// If no notifications of intrest is given, the notifications are turned off.
        /// </param>
        public void Subscribe(params string[] notificationsOfIntrest)
        {
            Send("subscribe " + string.Join(",", notificationsOfIntrest));
        }

        /// <summary>
        /// Set and query the Squeezebox Server's internal preference values.
        /// </summary>
        /// <param name="prefrenceName">
        /// If you want to query/set a preference from an other namespace than "server" (eg. a plugin),
        /// you'll have to prepend the desired namespace to the prefrenceName.
        /// </param>
        /// <param name="preferenceValue">
        /// Using '?' will query the preference value instead of setting it.
        /// </param>
        public void Pref(string prefrenceName, string preferenceValue)
        {
            Send("pref " + prefrenceName + " " + preferenceValue);
        }

        /// <summary>
        /// Validate a Squeezebox Server's internal preference value without setting it.
        /// </summary>
        /// <param name="prefrenceName">
        /// If you want to validate a preference from an other namespace than "server" (eg. a plugin), 
        /// you'll have to prepend the desired namespace to the prefrenceName.
        /// </param>
        /// <param name="preferenceValue"></param>
        public void PrefValidate(string prefrenceName, string preferenceValue)
        {
            Send("pref validate " + prefrenceName + " " + preferenceValue);
        }

        /// <summary>
        /// Set the logging level.
        /// </summary>
        /// <param name="loggingGroup"></param>
        /// <param name="persist"></param>
        public void Logging(LoggingGroup loggingGroup, bool persist)
        {
            string command = "logging group:" + loggingGroup.ToString().ToLower();

            if (persist)
            {
                command += " 1";
            }

            Send(command);
        }

        /// <summary>
        /// Query one or several localized strings.
        /// </summary>
        /// <param name="tokens"></param>
        public void GetString(params string[] tokens)
        {
            Send("getstring " + string.Join(",", tokens));
        }

        /// <summary>
        /// Change the credentials used to acces mysqueezebox.com resources.
        /// </summary>
        /// <param name="username"></param>
        /// <param name="password"></param>
        /// <param name="sync"></param>
        public void SetSnCredentials(string username, string password, bool sync)
        {
            Send("setsncredentials " + username + " " + password);
        }

        /// <summary>
        /// Query or set the server's internal debugging categories.
        /// </summary>
        /// <param name="debugCategory"></param>
        /// <param name="debugLevel"></param>
        public void Debug(string debugCategory, DebugLevel debugLevel)
        {
            string command = "debug " + debugCategory + " " + (int)debugLevel;
            Send(command);
        }

        /// <summary>
        /// Closes the TCP connection with the server and terminates the CLI session.
        /// </summary>
        public void Exit()
        {
            Send("exit");
        }

        /// <summary>
        /// Shuts down the server. The CLI connection is terminated.
        /// </summary>
        public void Shutdown()
        {
            Send("shutdown");
        }
        #endregion

        #region Players commands and queries
        /// <summary>
        /// Query the number of players connected to the server.
        /// </summary>
        public void PlayerCount()
        {
            Send("player count ?");
        }

        /// <summary>
        /// Query the unique identifier of a player.
        /// For physical players this is generally the MAC address. 
        /// The IP address is used for remote streams.
        /// </summary>
        /// <param name="playerIndex"></param>
        public void PlayerID(int playerIndex)
        {
            Send("player id " + playerIndex + " ?");
        }

        /// <summary>
        /// Query the player uuid. The uuid is used by mysqueezebox.com.
        /// </summary>
        /// <param name="playerIndex"></param>
        public void PlayerUUID(int playerIndex)
        {
            Send("player uuid " + playerIndex + " ?");
        }

        /// <summary>
        /// Query the human-readable name for the specified player. 
        /// If the name has not been specified by the user in the Player Settings, 
        /// then a default name will be used, usually the IP address.
        /// </summary>
        /// <param name="playerIndexOrID"></param>
        public void PlayerName(string playerIndexOrID)
        {
            Send("player name " + playerIndexOrID + " ?");
        }

        /// <summary>
        /// Query the IP address (along with port number) of the specified player.
        /// </summary>
        /// <param name="playerIndexOrID"></param>
        public void PlayerIP(string playerIndexOrID)
        {
            Send("player ip " + playerIndexOrID + " ?");
        }

        /// <summary>
        /// Queries the model of the player, 
        /// currently one of "transporter", "squeezebox2", "squeezebox", "slimp3", "softsqueeze", or "http" (for remote streaming connections).
        /// </summary>
        /// <param name="playerIndexOrID"></param>
        public void PlayerModel(string playerIndexOrID)
        {
            Send("player model " + playerIndexOrID + " ?");
        }

        /// <summary>
        /// Queries whether a player is a known player model. 
        /// Currently know models are "transporter", "squeezebox2", "squeezebox", "slimp3", "softsqueeze", or "http" (for remote streaming connections). 
        /// Will return 0 for streaming connections.
        /// </summary>
        /// <param name="playerIndexOrID"></param>
        public void PlayerIsPlayer(string playerIndexOrID)
        {
            Send("player isplayer " + playerIndexOrID + " ?");
        }

        /// <summary>
        /// Queries the display model of the player. 
        /// Graphical display types start with "graphic-", non-graphical display type with "noritake-".
        /// </summary>
        /// <param name="playerIndexOrID"></param>
        public void PlayerDisplayType(string playerIndexOrID)
        {
            Send("player displaytype " + playerIndexOrID + " ?");
        }

        /// <summary>
        /// Queries wether a player can be powered off or not. 
        /// Current hardware players and SoftSqueeze would return 1, web clients 0.
        /// </summary>
        /// <param name="playerIndexOrID"></param>
        public void PlayerCanPowerOff(string playerIndexOrID)
        {
            Send("player canpoweroff " + playerIndexOrID + " ?");
        }

        /// <summary>
        /// Queries information about all "players" (physical players as well as streaming clients) 
        /// known by the Squeezebox Server.
        /// </summary>
        /// <param name="start"></param>
        /// <param name="itemsPerResponse"></param>
        /// <param name="taggedParameters"></param>
        public void UpdatePlayers(int start, int itemsPerResponse, params PlayersTaggedParameter[] taggedParameters)
        {
            Send("players " + start + " " + itemsPerResponse + " " + string.Join<PlayersTaggedParameter>(" ", taggedParameters));
        }

        #endregion
    }
}
